<?php
package("Utils");
package("Utils_Xml");

define("JUNCTION_XML_HANDLE", "handle");

/**
 * Provide a parser which traverses a XML tree and employs handlers
 *
 * <p>The XML will be used to represent data, as such it will not 
 * contain any character data.  All character data will be ignored
 * by this parser.
 * <p>Handlers need to be set in order for any tags to be handled.
 * Tags which have a handler assigned will trigger the handler to
 * act when the tag ends.
 * 
 * @package junction.utils.xml
 * @author Alexander Schearer <aas11@duke.edu>
 */
class Utils_Xml_Parser {
	
	/**
	 * Stack
	 *
	 * @var Utils_Stack
	 */
	private $_stack;
	
	/**
	 * Hashmap of handlers for XML tags
	 *
	 * @var Utils_HashMap
	 */
	private $_handlers;
	
	/**
	 * Queue storing handler results
	 * 
	 * @var Utils_Stack
	 */
	private $_results;
	
	/**
	 * Create a new instance of the XML parser
	 * 
	 * @throws Utils_Xml_Exception in the event that the resource manager fails
	 */
	public function __construct() {
		$this->_stack = new Utils_Stack();
		$this->_handlers = new Utils_HashMap();
		$this->_results = new Utils_Stack();
	}
	
	/**
	 * Specify a handler to handle a XML tag
	 * 
	 * <p>The handler should implement the Utils_Xml_Handler interface.  It
	 * will be called to handle the specified tag when the tag closes.
	 *
	 * @param String $tag
	 * @param Utils_Xml_Exception $handler
	 */
	public function setHandler($tag, Utils_Xml_Handler $handler) {
		$this->_handlers->put($tag, $handler);
	}
	
	/**
	 * Parse a given XML file
	 *
	 * @throws Utils_Xml_Exception
	 * 
	 * @param String $file
	 * @return Utils_Stack
	 */
	public function parseFile($file) {
		if (!file_exists($file) || !is_readable($file)) {
			throw new Utils_Xml_Exception("File could not be opened: " . $file);
		}
		return $this->parse(file_get_contents($file));
	}
	
	/**
	 * Parse a given XML string
	 * 
	 * @throws Utils_Xml_Exception
	 *
	 * @param String $string
	 * @return Utils_Stack
	 */
	public function parse($string) {
		$parser = xml_parser_create();
		xml_set_object($parser, $this);
		xml_set_element_handler($parser, "startElement", "endElement");
		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
		if (!xml_parse($parser, $string)) {
			throw new Utils_Xml_Exception(xml_error_string(xml_get_error_code($parser)));
		}
        xml_parser_free($parser);
        return $this->_results;
	}
	
	/**
	 * Record the tag and its attributes
	 *
	 * @param resource $parser
	 * @param String $tag
	 * @param array $attributes
	 */
	private function startElement($parser, $tag, $attributes) {
		$this->_stack->push(array("tag" => $tag, "attributes" => $attributes));
	}
	
	/**
	 * Call the tag's corresponding handler and pass any attributes
	 *
	 * @param resource $parser
	 * @param String $tag
	 */
	private function endElement($parser, $tag) {
		try {
			$handler = $this->_handlers->get($tag);
			if (isset($handler))
				$this->_results->push(Utils_Reflection_Facade::invokeArgs($handler, JUNCTION_XML_HANDLE, $this->_stack->pop()));
		} catch (Utils_Reflection_Exception $e) {
			throw new Utils_Xml_Exception($e->getMessage());
		}
	}
}
?>